/*!
  jQuery Wookmark plugin
  @name jquery.wookmark.js
  @author Christoph Ono (chri@sto.ph or @gbks)
  @author Sebastian Helzle (sebastian@helzle.net or @sebobo)
  @version 1.4.3
  @date 8/25/2013
  @category jQuery plugin
  @copyright (c) 2009-2013 Christoph Ono (www.wookmark.com)
  @license Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
*/
;(function (factory) {
  if (typeof define === 'function' && define.amd) define(['jquery'], factory)
  else factory(jQuery)
})(function ($) {
  var Wookmark, defaultOptions, __bind

  __bind = function (fn, me) {
    return function () {
      return fn.apply(me, arguments)
    }
  }

  // Wookmark default options
  defaultOptions = {
    align: 'center',
    autoResize: false,
    comparator: null,
    container: $('body'),
    ignoreInactiveItems: true,
    itemWidth: 0,
    fillEmptySpace: false,
    flexibleWidth: 0,
    direction: undefined,
    offset: 2,
    onLayoutChanged: undefined,
    outerOffset: 0,
    resizeDelay: 50,
  }

  Wookmark = (function () {
    function Wookmark(handler, options) {
      // Instance variables.
      this.handler = handler
      this.columns = this.containerWidth = this.resizeTimer = null
      this.activeItemCount = 0
      this.itemHeightsDirty = true
      this.placeholders = []

      $.extend(true, this, defaultOptions, options)

      // Bind instance methods
      this.update = __bind(this.update, this)
      this.onResize = __bind(this.onResize, this)
      this.onRefresh = __bind(this.onRefresh, this)
      this.getItemWidth = __bind(this.getItemWidth, this)
      this.layout = __bind(this.layout, this)
      this.layoutFull = __bind(this.layoutFull, this)
      this.layoutColumns = __bind(this.layoutColumns, this)
      this.filter = __bind(this.filter, this)
      this.clear = __bind(this.clear, this)
      this.getActiveItems = __bind(this.getActiveItems, this)
      this.refreshPlaceholders = __bind(this.refreshPlaceholders, this)
      this.sortElements = __bind(this.sortElements, this)

      // Collect filter data
      var i = 0,
        j = 0,
        filterClasses = {},
        itemFilterClasses,
        $item,
        filterClass

      for (; i < handler.length; i++) {
        $item = handler.eq(i)

        // Read filter classes
        itemFilterClasses = $item.data('filterClass')

        // Globally store each filter class as object and the fitting items in the array
        if (
          typeof itemFilterClasses == 'object' &&
          itemFilterClasses.length > 0
        ) {
          for (j = 0; j < itemFilterClasses.length; j++) {
            filterClass = $.trim(itemFilterClasses[j]).toLowerCase()

            if (!(filterClass in filterClasses)) {
              filterClasses[filterClass] = []
            }
            filterClasses[filterClass].push($item[0])
          }
        }
      }
      this.filterClasses = filterClasses

      // Listen to resize event if requested.
      if (this.autoResize) $(window).bind('resize.wookmark', this.onResize)

      this.container.bind('refreshWookmark', this.onRefresh)
    }

    // Method for updating the plugins options
    Wookmark.prototype.update = function (options) {
      this.itemHeightsDirty = true
      $.extend(true, this, options)
    }

    // This timer ensures that layout is not continuously called as window is being dragged.
    Wookmark.prototype.onResize = function () {
      clearTimeout(this.resizeTimer)
      this.itemHeightsDirty = this.flexibleWidth !== 0
      this.resizeTimer = setTimeout(this.layout, this.resizeDelay)
    }

    // Marks the items heights as dirty and does a relayout
    Wookmark.prototype.onRefresh = function () {
      this.itemHeightsDirty = true
      this.layout()
    }

    /**
     * Filters the active items with the given string filters.
     * @param filters array of string
     * @param mode 'or' or 'and'
     */
    Wookmark.prototype.filter = function (filters, mode) {
      var activeFilters = [],
        activeFiltersLength,
        activeItems = $(),
        i,
        j,
        k,
        filter

      filters = filters || []
      mode = mode || 'or'

      if (filters.length) {
        // Collect active filters
        for (i = 0; i < filters.length; i++) {
          filter = $.trim(filters[i].toLowerCase())
          if (filter in this.filterClasses) {
            activeFilters.push(this.filterClasses[filter])
          }
        }

        // Get items for active filters with the selected mode
        activeFiltersLength = activeFilters.length
        if (mode == 'or' || activeFiltersLength == 1) {
          // Set all items in all active filters active
          for (i = 0; i < activeFiltersLength; i++) {
            activeItems = activeItems.add(activeFilters[i])
          }
        } else if (mode == 'and') {
          var shortestFilter = activeFilters[0],
            itemValid = true,
            foundInFilter,
            currentItem,
            currentFilter

          // Find shortest filter class
          for (i = 1; i < activeFiltersLength; i++) {
            if (activeFilters[i].length < shortestFilter.length) {
              shortestFilter = activeFilters[i]
            }
          }

          // Iterate over shortest filter and find elements in other filter classes
          for (i = 0; i < shortestFilter.length; i++) {
            currentItem = shortestFilter[i]
            itemValid = true

            for (j = 0; j < activeFilters.length && itemValid; j++) {
              currentFilter = activeFilters[j]
              if (shortestFilter == currentFilter) continue

              // Search for current item in each active filter class
              for (
                k = 0, foundInFilter = false;
                k < currentFilter.length && !foundInFilter;
                k++
              ) {
                foundInFilter = currentFilter[k] == currentItem
              }
              itemValid &= foundInFilter
            }
            if (itemValid) activeItems.push(shortestFilter[i])
          }
        }
        // Hide inactive items
        this.handler.not(activeItems).addClass('inactive')
      } else {
        // Show all items if no filter is selected
        activeItems = this.handler
      }

      // Show active items
      activeItems.removeClass('inactive')

      // Unset columns and refresh grid for a full layout
      this.columns = null
      this.layout()
    }

    /**
     * Creates or updates existing placeholders to create columns of even height
     */
    Wookmark.prototype.refreshPlaceholders = function (
      columnWidth,
      sideOffset
    ) {
      var i = this.placeholders.length,
        $placeholder,
        $lastColumnItem,
        columnsLength = this.columns.length,
        column,
        height,
        top,
        innerOffset,
        containerHeight = this.container.innerHeight()

      for (; i < columnsLength; i++) {
        $placeholder = $('<div class="wookmark-placeholder"/>').appendTo(
          this.container
        )
        this.placeholders.push($placeholder)
      }

      innerOffset =
        this.offset + parseInt(this.placeholders[0].css('borderWidth'), 10) * 2

      for (i = 0; i < this.placeholders.length; i++) {
        $placeholder = this.placeholders[i]
        column = this.columns[i]

        if (i >= columnsLength || !column[column.length - 1]) {
          $placeholder.css('display', 'none')
        } else {
          $lastColumnItem = column[column.length - 1]
          if (!$lastColumnItem) continue
          top =
            $lastColumnItem.data('wookmark-top') +
            $lastColumnItem.data('wookmark-height') +
            this.offset
          height = containerHeight - top - innerOffset

          $placeholder.css({
            position: 'absolute',
            display: height > 0 ? 'block' : 'none',
            left: i * columnWidth + sideOffset,
            top: top,
            width: columnWidth - innerOffset,
            height: height,
          })
        }
      }
    }

    // Method the get active items which are not disabled and visible
    Wookmark.prototype.getActiveItems = function () {
      return this.ignoreInactiveItems
        ? this.handler.not('.inactive')
        : this.handler
    }

    // Method to get the standard item width
    Wookmark.prototype.getItemWidth = function () {
      var itemWidth = this.itemWidth,
        innerWidth = this.container.width() - 2 * this.outerOffset,
        firstElement = this.handler.eq(0),
        flexibleWidth = this.flexibleWidth

      if (
        this.itemWidth === undefined ||
        (this.itemWidth === 0 && !this.flexibleWidth)
      ) {
        itemWidth = firstElement.outerWidth()
      } else if (
        typeof this.itemWidth == 'string' &&
        this.itemWidth.indexOf('%') >= 0
      ) {
        itemWidth = (parseFloat(this.itemWidth) / 100) * innerWidth
      }

      // Calculate flexible item width if option is set
      if (flexibleWidth) {
        if (
          typeof flexibleWidth == 'string' &&
          flexibleWidth.indexOf('%') >= 0
        ) {
          flexibleWidth = (parseFloat(flexibleWidth) / 100) * innerWidth
        }

        var columns = ~~(
            0.5 +
            (innerWidth + this.offset) / (flexibleWidth + this.offset)
          ),
          columnWidth = Math.min(
            flexibleWidth,
            ~~((innerWidth - (columns - 1) * this.offset) / columns)
          )

        itemWidth = Math.max(itemWidth, columnWidth)

        // Stretch items to fill calculated width
        this.handler.css('width', itemWidth)
      }

      return itemWidth
    }

    // Main layout method.
    Wookmark.prototype.layout = function (force) {
      // Do nothing if container isn't visible
      if (!this.container.is(':visible')) return

      // Calculate basic layout parameters.
      var columnWidth = this.getItemWidth() + this.offset,
        containerWidth = this.container.width(),
        innerWidth = containerWidth - 2 * this.outerOffset,
        columns = ~~((innerWidth + this.offset) / columnWidth),
        offset = 0,
        maxHeight = 0,
        i = 0,
        activeItems = this.getActiveItems(),
        activeItemsLength = activeItems.length,
        $item

      // Cache item height
      if (this.itemHeightsDirty) {
        for (; i < activeItemsLength; i++) {
          $item = activeItems.eq(i)
          $item.data('wookmark-height', $item.outerHeight())
        }
        this.itemHeightsDirty = false
      }

      // Use less columns if there are to few items
      columns = Math.max(1, Math.min(columns, activeItemsLength))

      // Calculate the offset based on the alignment of columns to the parent container
      offset = this.outerOffset
      if (this.align == 'center') {
        offset += ~~(
          (0.5 + (innerWidth - (columns * columnWidth - this.offset))) >>
          1
        )
      }

      // Get direction for positioning
      this.direction =
        this.direction || (this.align == 'right' ? 'right' : 'left')

      // If container and column count hasn't changed, we can only update the columns.
      if (
        !force &&
        this.columns !== null &&
        this.columns.length == columns &&
        this.activeItemCount == activeItemsLength
      ) {
        maxHeight = this.layoutColumns(columnWidth, offset)
      } else {
        maxHeight = this.layoutFull(columnWidth, columns, offset)
      }
      this.activeItemCount = activeItemsLength

      // Set container height to height of the grid.
      this.container.css('height', maxHeight)

      // Update placeholders
      if (this.fillEmptySpace) {
        this.refreshPlaceholders(columnWidth, offset)
      }

      if (
        this.onLayoutChanged !== undefined &&
        typeof this.onLayoutChanged === 'function'
      ) {
        this.onLayoutChanged()
      }
    }

    /**
     * Sort elements with configurable comparator
     */
    Wookmark.prototype.sortElements = function (elements) {
      return typeof this.comparator === 'function'
        ? elements.sort(this.comparator)
        : elements
    }

    /**
     * Perform a full layout update.
     */
    Wookmark.prototype.layoutFull = function (columnWidth, columns, offset) {
      var $item,
        i = 0,
        k = 0,
        activeItems = $.makeArray(this.getActiveItems()),
        length = activeItems.length,
        shortest = null,
        shortestIndex = null,
        itemCSS = { position: 'absolute' },
        sideOffset,
        heights = [],
        leftAligned = this.align == 'left' ? true : false

      this.columns = []

      // Sort elements before layouting
      activeItems = this.sortElements(activeItems)

      // Prepare arrays to store height of columns and items.
      while (heights.length < columns) {
        heights.push(this.outerOffset)
        this.columns.push([])
      }

      // Loop over items.
      for (; i < length; i++) {
        $item = $(activeItems[i])

        // Find the shortest column.
        shortest = heights[0]
        shortestIndex = 0
        for (k = 0; k < columns; k++) {
          if (heights[k] < shortest) {
            shortest = heights[k]
            shortestIndex = k
          }
        }

        // stick to left side if alignment is left and this is the first column
        sideOffset = offset
        if (shortestIndex > 0 || !leftAligned)
          sideOffset += shortestIndex * columnWidth

        // Position the item.
        itemCSS[this.direction] = sideOffset
        itemCSS.top = shortest
        $item.css(itemCSS).data('wookmark-top', shortest)

        // Update column height and store item in shortest column
        heights[shortestIndex] += $item.data('wookmark-height') + this.offset
        this.columns[shortestIndex].push($item)
      }

      // Return longest column
      return Math.max.apply(Math, heights)
    }

    /**
     * This layout method only updates the vertical position of the
     * existing column assignments.
     */
    Wookmark.prototype.layoutColumns = function (columnWidth, offset) {
      var heights = [],
        i = 0,
        k = 0,
        currentHeight,
        column,
        $item,
        itemCSS,
        sideOffset

      for (; i < this.columns.length; i++) {
        heights.push(this.outerOffset)
        column = this.columns[i]
        sideOffset = i * columnWidth + offset
        currentHeight = heights[i]

        for (k = 0; k < column.length; k++) {
          $item = column[k]
          itemCSS = {
            top: currentHeight,
          }
          itemCSS[this.direction] = sideOffset

          $item.css(itemCSS).data('wookmark-top', currentHeight)

          currentHeight += $item.data('wookmark-height') + this.offset
        }
        heights[i] = currentHeight
      }

      // Return longest column
      return Math.max.apply(Math, heights)
    }

    /**
     * Clear event listeners and time outs.
     */
    Wookmark.prototype.clear = function () {
      clearTimeout(this.resizeTimer)
      $(window).unbind('resize.wookmark', this.onResize)
      this.container.unbind('refreshWookmark', this.onRefresh)
    }

    return Wookmark
  })()

  $.fn.wookmark = function (options) {
    // Create a wookmark instance if not available
    if (!this.wookmarkInstance) {
      this.wookmarkInstance = new Wookmark(this, options || {})
    } else {
      this.wookmarkInstance.update(options || {})
    }

    // Apply layout
    this.wookmarkInstance.layout(true)

    // Display items (if hidden) and return jQuery object to maintain chainability
    return this.show()
  }
})
